探索使用 postMessage API 进行安全的跨源通信。了解其功能、安全风险以及减轻 Web 应用程序漏洞的最佳实践。
跨源通信:使用 postMessage API 的安全模式
在现代 Web 中,应用程序经常需要与来自不同源的资源进行交互。同源策略 (Same-Origin Policy, SOP) 是一种关键的安全机制,它限制脚本访问来自不同源的资源。然而,在某些合法场景下,跨源通信是必要的。postMessage API 为实现此目的提供了一种受控机制,但理解其潜在的安全风险并实施适当的安全模式至关重要。
理解同源策略 (SOP)
同源策略是 Web 浏览器中的一个基本安全概念。它限制网页向与其来源域不同的域发出请求。源由方案(协议)、主机(域名)和端口定义。如果其中任何一个不同,则认为源是不同的。例如:
https://example.comhttps://www.example.comhttp://example.comhttps://example.com:8080
这些都是不同的源,SOP 限制了它们之间的直接脚本访问。
PostMessage API 简介
postMessage API 为跨源通信提供了一种安全且受控的机制。它允许脚本向其他窗口(例如,iframe、新窗口或标签页)发送消息,无论它们的源是什么。接收窗口可以监听这些消息并相应地处理它们。
发送消息的基本语法是:
otherWindow.postMessage(message, targetOrigin);
otherWindow: 对目标窗口的引用(例如,window.parent、iframe.contentWindow或从window.open获取的窗口对象)。message: 您想要发送的数据。这可以是任何可以被序列化的 JavaScript 对象(例如,字符串、数字、对象、数组)。targetOrigin: 指定您希望将消息发送到的源。这是一个关键的安全参数。
在接收端,您需要监听 message 事件:
window.addEventListener('message', function(event) {
// ...
});
event 对象包含以下属性:
event.data: 另一个窗口发送的消息。event.origin: 发送消息的窗口的源。event.source: 对发送消息的窗口的引用。
安全风险与漏洞
虽然 postMessage 提供了一种绕过 SOP 限制的方法,但如果实施不当,也会引入潜在的安全风险。以下是一些常见的漏洞:
1. 目标源不匹配
未能验证 event.origin 属性是一个严重的漏洞。如果接收方盲目信任消息,任何网站都可以发送恶意数据。在处理消息之前,请务必验证 event.origin 是否与预期的源匹配。
示例(易受攻击的代码):
window.addEventListener('message', function(event) {
// 不要这样做!
processMessage(event.data);
});
示例(安全代码):
window.addEventListener('message', function(event) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Received message from untrusted origin:', event.origin);
return;
}
processMessage(event.data);
});
2. 数据注入
将接收到的数据(event.data)视为可执行代码或直接注入到 DOM 中,可能导致跨站脚本(XSS)漏洞。在使用接收到的数据之前,务必对其进行净化和验证。
示例(易受攻击的代码):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
document.body.innerHTML = event.data; // 不要这样做!
}
});
示例(安全代码):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
const sanitizedData = sanitize(event.data); // 实现一个适当的净化函数
document.getElementById('message-container').textContent = sanitizedData;
}
});
function sanitize(data) {
// 在此处实现健壮的净化逻辑。
// 例如,使用 DOMPurify 或类似的库
return DOMPurify.sanitize(data);
}
3. 中间人(MITM)攻击
如果通信通过不安全的通道(HTTP)进行,中间人攻击者可以拦截并修改消息。始终使用 HTTPS 进行安全通信。
4. 跨站请求伪造(CSRF)
如果接收方在没有适当验证的情况下根据接收到的消息执行操作,攻击者可能会伪造消息以欺骗接收方执行非预期的操作。实施 CSRF 保护机制,例如在消息中包含一个秘密令牌并在接收方进行验证。
5. 在 targetOrigin 中使用通配符
将 targetOrigin 设置为 * 允许任何源接收消息。除非绝对必要,否则应避免这样做,因为它违背了基于源的安全目的。如果您必须使用 *,请确保实施其他强有力的安全措施,例如消息认证码(MAC)。
示例(避免此做法):
otherWindow.postMessage(message, '*'); // 除非绝对必要,否则避免使用 '*'
安全模式与最佳实践
为了减轻与 postMessage 相关的风险,请遵循以下安全模式和最佳实践:
1. 严格的源验证
始终在接收方验证 event.origin 属性。将其与预定义的受信任源列表进行比较。使用严格相等(===)进行比较。
2. 数据净化与验证
在使用通过 postMessage 接收的所有数据之前,对其进行净化和验证。根据数据的使用方式,采用适当的净化技术(例如,HTML 转义、URL 编码、输入验证)。使用像 DOMPurify 这样的库来净化 HTML。
3. 消息认证码 (MAC)
在消息中包含消息认证码(MAC)以确保其完整性和真实性。发送方使用共享密钥计算 MAC 并将其包含在消息中。接收方使用相同的共享密钥重新计算 MAC,并将其与接收到的 MAC 进行比较。如果它们匹配,则认为消息是真实且未被篡改的。
示例(使用 HMAC-SHA256):
// 发送方
async function sendMessage(message, targetOrigin, sharedSecret) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: message,
signature: signatureHex
};
otherWindow.postMessage(securedMessage, targetOrigin);
}
// 接收方
async function receiveMessage(event, sharedSecret) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Received message from untrusted origin:', event.origin);
return;
}
const securedMessage = event.data;
const message = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Message is authentic!');
processMessage(message); // 继续处理消息
} else {
console.error('Message signature verification failed!');
}
}
重要提示:共享密钥必须安全地生成和存储。避免在代码中硬编码密钥。
4. 使用 Nonce 和时间戳
为了防止重放攻击,在消息中包含一个唯一的 nonce(一次性数字)和一个时间戳。接收方可以验证该 nonce 此前未使用过,并且时间戳在可接受的时间范围内。这减轻了攻击者重放先前截获的消息的风险。
5. 最小权限原则
仅授予其他窗口最小的必要权限。例如,如果其他窗口只需要读取数据,则不要允许它写入数据。在设计通信协议时要牢记最小权限原则。
6. 内容安全策略 (CSP)
使用内容安全策略 (CSP) 来限制可以加载脚本的源以及脚本可以执行的操作。这有助于减轻因不当处理 postMessage 数据而可能产生的 XSS 漏洞的影响。
7. 输入验证
始终验证接收数据的结构和格式。定义明确的消息格式,并确保接收到的数据符合此格式。这有助于防止意外行为和漏洞。
8. 安全的数据序列化
使用安全的数据序列化格式(如 JSON)来序列化和反序列化消息。避免使用允许代码执行的格式,例如 eval() 或 Function()。
9. 限制消息大小
限制通过 postMessage 发送的消息的大小。大消息会消耗过多资源,并可能导致拒绝服务攻击。
10. 定期安全审计
定期对您的代码进行安全审计,以识别和解决潜在的漏洞。特别注意 postMessage 的实现,并确保遵循所有安全最佳实践。
示例场景:Iframe 与其父页面之间的安全通信
考虑一个场景,其中托管在 https://iframe.example.com 上的 iframe 需要与其托管在 https://parent.example.com 上的父页面通信。该 iframe 需要将用户数据发送到父页面进行处理。
Iframe (https://iframe.example.com):
// 生成共享密钥(请替换为安全的密钥生成方法)
const sharedSecret = 'YOUR_SECURE_SHARED_SECRET';
// 获取用户数据
const userData = {
name: 'John Doe',
email: 'john.doe@example.com'
};
// 将用户数据发送到父页面
async function sendUserData(userData) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: userData,
signature: signatureHex
};
parent.postMessage(securedMessage, 'https://parent.example.com');
}
sendUserData(userData);
父页面 (https://parent.example.com):
// 共享密钥(必须与 iframe 的密钥匹配)
const sharedSecret = 'YOUR_SECURE_SHARED_SECRET';
window.addEventListener('message', async function(event) {
if (event.origin !== 'https://iframe.example.com') {
console.warn('Received message from untrusted origin:', event.origin);
return;
}
const securedMessage = event.data;
const userData = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Message is authentic!');
// 处理用户数据
console.log('User data:', userData);
} else {
console.error('Message signature verification failed!');
}
});
重要说明:
- 将
YOUR_SECURE_SHARED_SECRET替换为安全生成的共享密钥。 - 共享密钥在 iframe 和父页面中必须相同。
- 此示例使用 HMAC-SHA256 进行消息认证。
结论
postMessage API 是一个强大的工具,用于在 Web 应用程序中实现跨源通信。然而,理解潜在的安全风险并实施适当的安全模式以减轻这些风险至关重要。通过遵循本指南中概述的安全模式和最佳实践,您可以安全地使用 postMessage 来构建健壮且安全的 Web 应用程序。
请记住始终优先考虑安全性,并随时了解 Web 开发的最新安全最佳实践。定期审查您的代码和安全配置,以确保您的应用程序受到保护,免受潜在漏洞的侵害。